﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;

namespace FlightToolkit.SimProp
{
    public class Property
    {
        #region Public
        public Type Type
        {
            get
            {
                return this.def.Type.Type;
            }
        }

        public string Name
        {
            get
            {
                return this.def.Name;
            }
        }

        public string NamespacedName
        {
            get
            {
                return String.Format("{0}.{1}", this.def.namespaceDef.Name, this.def.Name);
            }
        }

        public bool Attribute
        {
            get
            {
                return this.def.SerializeAsXmlAttribute;
            }
        }

        public Guid DefId
        {
            get
            {
                return this.def.Id;
            }
        }

        public Guid LocalizationId
        {
            get
            {
                return this.locid;
            }
        }

        public string String
        {
            get
            {
                if (this.def.Type.Type != Type.String)
                {
                    throw new InvalidDataException("Property is not of type String");
                }

                return (string)this.data;
            }
            set
            {
                if (this.def.Type.Type != Type.String)
                {
                    throw new InvalidDataException("Property is not of type String");
                }

                this.data = value.Trim();
                if (this.def.Localize == Localize.Yes)
                {
                    locid = Guid.NewGuid();
                }
            }
        }

        public Guid Guid
        {
            get
            {
                if (this.def.Type.Type != Type.Guid)
                {
                    throw new InvalidDataException("Property is not of type Guid");
                }

                var guidData = (Guid[])this.data;
                return guidData[0];
            }
            set
            {
                if (this.def.Type.Type != Type.Guid)
                {
                    throw new InvalidDataException("Property is not of type Guid");
                }

                var guidData = (Guid[])this.data;
                guidData[0] = value;
            }
        }

        public DateTime Filetime
        {
            get
            {
                if (this.def.Type.Type != Type.Filetime)
                {
                    throw new InvalidDataException("Property is not of type Filetime");
                }

                var datetimeData = (DateTime[])this.data;
                return datetimeData[0];
            }
            set
            {
                if (this.def.Type.Type != Type.Filetime)
                {
                    throw new InvalidDataException("Property is not of type Filetime");
                }

                var datetimeData = (DateTime[])this.data;
                datetimeData[0] = value;
            }
        }

        public byte[] Byte
        {
            get
            {
                if (this.def.Type.Type != Type.Byte4)
                {
                    throw new InvalidDataException("Property is not of type Byte[]");
                }

                return (byte[])this.data;
            }
            private set
            {
                if (this.def.Type.Type != Type.Byte4)
                {
                    throw new InvalidDataException("Property is not of type Byte[]");
                }

                if (value.Length != 4)
                {
                    throw new ArgumentException("Must be an array of 4", "value");
                }

                this.data = value;
            }
        }

        public int[] Int
        {
            get
            {
                if (this.def.Type.Type != Type.Long &&
                    this.def.Type.Type != Type.Long2 &&
                    this.def.Type.Type != Type.Long3 &&
                    this.def.Type.Type != Type.Long4 &&
                    this.def.Type.Type != Type.Bool &&
                    this.def.Type.Type != Type.ULong &&
                    this.def.Type.Type != Type.Enum)
                {
                    throw new InvalidDataException("Property is not of type Bool, Enum, ULong, Long or Long[]");
                }

                return (int[])this.data;
            }
            private set
            {
                if (this.def.Type.Type != Type.Long &&
                    this.def.Type.Type != Type.Long2 &&
                    this.def.Type.Type != Type.Long3 &&
                    this.def.Type.Type != Type.Long4 &&
                    this.def.Type.Type != Type.Bool &&
                    this.def.Type.Type != Type.ULong &&
                    this.def.Type.Type != Type.Enum)
                {
                    throw new InvalidDataException("Property is not of type Bool, Enum, ULong, Long or Long[]");
                }

                if ((this.def.Type.Type == Type.Long4 && value.Length != 4) ||
                    (this.def.Type.Type == Type.Long3 && value.Length != 3) ||
                    (this.def.Type.Type == Type.Long2 && value.Length != 2) ||
                    (this.def.Type.Type == Type.Long && value.Length != 1) ||
                    (this.def.Type.Type == Type.ULong && value.Length != 1) ||
                    (this.def.Type.Type == Type.Bool && value.Length != 1) ||
                    (this.def.Type.Type == Type.Enum && value.Length != 1))
                {
                    throw new ArgumentException("Array is incorrect length", "value");
                }

                this.data = value;
            }
        }

        public float[] Float
        {
            get
            {
                if (this.def.Type.Type != Type.Float &&
                    this.def.Type.Type != Type.Float2 &&
                    this.def.Type.Type != Type.Float3 &&
                    this.def.Type.Type != Type.Float4 &&
                    this.def.Type.Type != Type.Matrix &&
                    this.def.Type.Type != Type.PBH)
                {
                    throw new InvalidDataException("Property is not of type PBH, Matrix, Float or Float[]");
                }

                return (float[])this.data;
            }
            private set
            {
                if (this.def.Type.Type != Type.Float &&
                    this.def.Type.Type != Type.Float2 &&
                    this.def.Type.Type != Type.Float3 &&
                    this.def.Type.Type != Type.Float4 &&
                    this.def.Type.Type != Type.Matrix &&
                    this.def.Type.Type != Type.PBH)
                {
                    throw new InvalidDataException("Property is not of type PBH, Matrix, Float or Float[]");
                }

                if ((this.def.Type.Type == Type.Matrix && value.Length != 16) || 
                    (this.def.Type.Type == Type.Float4 && value.Length != 4) ||
                    (this.def.Type.Type == Type.Float3 && value.Length != 3) ||
                    (this.def.Type.Type == Type.PBH && value.Length != 3) ||
                    (this.def.Type.Type == Type.Float2 && value.Length != 2) ||
                    (this.def.Type.Type == Type.Float && value.Length != 1))
                {
                    throw new ArgumentException("Array is incorrect length", "value");
                }

                this.data = value;
            }
        }

        public double[] Double
        {
            get
            {
                if (this.def.Type.Type != Type.Double &&
                    this.def.Type.Type != Type.LLA)
                {
                    throw new InvalidDataException("Property is not of type Double, or LLA");
                }

                return (double[])this.data;
            }
            private set
            {
                if (this.def.Type.Type != Type.Double &&
                    this.def.Type.Type != Type.LLA)
                {
                    throw new InvalidDataException("Property is not of type Double, or LLA");
                }

                if ((this.def.Type.Type == Type.LLA && value.Length != 3) ||
                    (this.def.Type.Type == Type.Double && value.Length != 1))
                {
                    throw new ArgumentException("Array is incorrect length", "value");
                }

                this.data = value;
            }
        }
        #endregion

        #region Internal
        internal PropertyDef def;

        internal Property(PropertyDef def)
        {
            this.def = def;
            switch (this.def.Type.Type)
            {
                case Type.Guid:
                    this.data = new Guid[1];
                    break;

                case Type.Filetime:
                    this.data = new DateTime[1];
                    break;

                case Type.Byte4:
                    this.data = new byte[4];
                    break;

                case Type.Enum:
                case Type.Bool:
                case Type.ULong:
                case Type.Long:
                    this.data = new int[1];
                    break;

                case Type.Long2:
                    this.data = new int[2];
                    break;

                case Type.Long3:
                    this.data = new int[3];
                    break;

                case Type.Long4:
                    this.data = new int[4];
                    break;

                case Type.Float:
                    this.data = new float[1];
                    break;

                case Type.Float2:
                    this.data = new float[2];
                    break;

                case Type.Float3:
                case Type.PBH:
                    this.data = new float[3];
                    break;

                case Type.Float4:
                    this.data = new float[4];
                    break;

                case Type.Matrix:
                    this.data = new float[16];
                    break;

                case Type.Double:
                    this.data = new double[1];
                    break;

                case Type.LLA:
                    this.data = new double[3];
                    break;
            }
        }

        internal void WriteXML(NamespaceDef currentNamespace, XmlTextWriter xw)
        {
            string namespaceText = "";
            if (currentNamespace.GetSymbolDef(this.def.Id) == null)
            {
                namespaceText = this.def.namespaceDef.Name + ".";
                currentNamespace = this.def.namespaceDef;
            }

            if (!this.def.SerializeAsXmlAttribute)
            {
                xw.WriteStartElement(namespaceText + this.def.Name);

                if (this.locid != Guid.Empty)
                {
                    xw.WriteAttributeString("_id", "{" + this.locid.ToString().ToUpper() + "}");
                }
            }

            string stringValue;
            switch (this.def.Type.Type)
            {
                case Type.Byte4:
                    {
                        var byteData = (byte[])this.data;
                        stringValue = string.Join(",", byteData.Select(n => n.ToString()).ToArray());
                    }
                    break;

                case Type.String:
                    stringValue = (string)this.data;
                    break;

                case Type.Guid:
                    {
                        var guidData = ((Guid[])this.data)[0];
                        stringValue = "{" + guidData.ToString().ToUpper() + "}";
                    }
                    break;

                case Type.Bool:
                    {
                        var boolData = ((int[])this.data)[0] != 0;
                        stringValue = boolData ? "True" : "False";
                    }
                    break;

                case Type.Enum:
                    {
                        var intData = ((int[])this.data)[0];
                        stringValue = Enum.GetNames(this.def.SystemType)[intData];
                    }
                    break;

                case Type.ULong:
                case Type.Long:
                case Type.Long2:
                case Type.Long3:
                case Type.Long4:
                    {
                        var intData = (int[])this.data;
                        stringValue = string.Join(",", intData.Select(n => n.ToString()).ToArray());
                    }
                    break;

                case Type.Filetime:
                    {
                        var datetimeData = ((DateTime[])this.data)[0];
                        stringValue = datetimeData.ToString("yyyy'-'MM'-'dd', 'HH':'mm':'ss':'fff");
                    }
                    break;

                case Type.Float:
                case Type.Float2:
                case Type.Float3:
                case Type.Float4:
                case Type.Matrix:
                case Type.PBH:
                    {
                        var floatData = (float[])this.data;
                        stringValue = string.Join(",", floatData.Select(n => n.ToString(CultureInfo.InvariantCulture)).ToArray());
                    }
                    break;

                case Type.Double:
                    {
                        var doubleData = ((double[])this.data)[0];
                        stringValue = doubleData.ToString(CultureInfo.InvariantCulture);
                    }
                    break;

                case Type.LLA:
                    {
                        var doubleData = (double[])this.data;
                        stringValue = String.Format(CultureInfo.InvariantCulture, "{0},{1},{2}", doubleData[0], doubleData[1], doubleData[2] / 0.3048); // XML stores alt as feet
                    }
                    break;

                default:
                    throw new InvalidDataException("Unknown data type");
            }

            if (this.def.SerializeAsXmlAttribute)
            {
                xw.WriteAttributeString(namespaceText + this.def.Name, stringValue);
            }
            else
            {
                xw.WriteString(stringValue);
                xw.WriteEndElement();
            }
        }

        internal void ReadXML(XmlTextReader xr)
        {
            if (xr.NodeType != XmlNodeType.Attribute && xr.HasAttributes)
            {
                while (xr.MoveToNextAttribute()) // Read the attributes.
                {
                    if (xr.Name == "_id")
                    {
                        this.locid = new Guid(xr.Value);
                    }
                }

                xr.MoveToElement();
            }

            string value = xr.HasValue ? xr.Value : xr.ReadElementString();

            switch (this.def.Type.Type)
            {
                case Type.Byte4:
                    {
                        this.Byte = value.Split(',').Select(n => Convert.ToByte(n)).ToArray();
                    }
                    break;

                case Type.Enum:
                    this.Int[0] = (int)Enum.Parse(this.def.SystemType, value);
                    break;

                case Type.Bool:
                    {
                        this.Int[0] = value.Equals("True", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
                    }
                    break;

                case Type.Filetime:
                    {
                        this.Filetime = DateTime.ParseExact(value, "yyyy'-'MM'-'dd', 'HH':'mm':'ss':'fff", CultureInfo.InvariantCulture);
                    }
                    break;

                case Type.ULong:
                case Type.Long:
                case Type.Long2:
                case Type.Long3:
                case Type.Long4:
                    {
                        this.Int = value.Split(',').Select(n => Convert.ToInt32(n)).ToArray();
                    }
                    break;

                case Type.Float:
                case Type.Float2:
                case Type.Float3:
                case Type.Float4:
                case Type.Matrix:
                case Type.PBH:
                    {
                        this.Float = value.Split(',').Select(n => Convert.ToSingle(n, CultureInfo.InvariantCulture)).ToArray();
                     }
                    break;

                case Type.Double:
                    {
                        this.Double = value.Split(',').Select(n => Convert.ToDouble(n, CultureInfo.InvariantCulture)).ToArray();
                    }
                    break;

                case Type.Guid:
                    {
                        this.Guid = new Guid(value);
                    }
                    break;

                case Type.String:
                    {
                        this.data = value;
                    }
                    break;

                case Type.LLA:
                    {
                        var doubleData = value.Split(',').Select(n => Convert.ToDouble(n, CultureInfo.InvariantCulture)).ToArray();
                        doubleData[2] *= 0.3048; // XML stores alt as feet, but in binary it is stored as meters
                        this.Double = doubleData;
                    }
                    break;

                case Type.PropertySet:
                default:
                    throw new InvalidDataException("UNEXPECTED TYPE FOR PROPERTY");
            }
        }

        internal void BuildGuidTable(OrderedDictionary<Guid, int> idToObjectSizeTable)
        {
            if (!idToObjectSizeTable.ContainsKey(this.def.Id))
            {
                idToObjectSizeTable.Add(this.def.Id, this.def.Type.GetTypeSize());
            }

            if (!idToObjectSizeTable.ContainsKey(this.locid))
            {
                idToObjectSizeTable.Add(this.locid, 0);
            }
        }

        internal void WriteBinaryData(OrderedDictionary<Guid, int> idToObjectSizeTable, Format format, BinaryWriter bw)
        {
            bw.Write((Int32)idToObjectSizeTable.IndexOfKey(this.def.Id));

            if (format == Format.FlightBinary)
            {
                if (this.locid != Guid.Empty)
                {
                    bw.Write((Int32)idToObjectSizeTable.IndexOfKey(this.locid));
                }
                else
                {
                    bw.Write((Int32)0);
                }
            }

            switch (this.def.Type.Type)
            {
                case Type.Byte4:
                    {
                        var byteData = this.Byte;
                        bw.Write((byte)byteData[0]);
                        bw.Write((byte)byteData[1]);
                        bw.Write((byte)byteData[2]);
                        bw.Write((byte)byteData[3]);
                    }
                    break;

                case Type.ULong:
                case Type.Enum:
                case Type.Bool:
                case Type.Long:
                case Type.Long2:
                case Type.Long3:
                case Type.Long4:
                    {
                        var intData = this.Int;
                        foreach (var i in intData)
                        {
                            bw.Write((Int32)i);
                        }
                    }
                    break;

                case Type.Filetime:
                    {
                        long b = this.Filetime.ToFileTimeUtc();
                        bw.Write((Int32)(b & uint.MaxValue));
                        bw.Write((Int32)(b >> 32));
                    }
                    break;

                case Type.Float:
                case Type.Float2:
                case Type.Float3:
                case Type.Float4:
                case Type.Matrix:
                    {
                        var floatData = this.Float;
                        foreach (var i in floatData)
                        {
                            bw.Write((Single)i);
                        }
                    }
                    break;

                case Type.Double:
                    {
                        var doubleData = this.Double;
                        bw.Write((Double)doubleData[0]);
                    }
                    break;

                case Type.Guid:
                    {
                        bw.Write(this.Guid.ToByteArray());
                    }
                    break;

                case Type.String:
                    {
                        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(this.String);
                        bw.Write((Int32)(bytes.Length + 1));
                        bw.Write(bytes);
                        bw.Write((byte)0);
                    }
                    break;

                case Type.LLA:
                    {
                        var doubleData = this.Double;
                        bw.Write((Int64)(doubleData[0] / ((90.0 / (40007000.0 / 4.0)) / Math.Pow(2, 32))) & ~0xFFFF);
                        bw.Write((Int64)(doubleData[1] / ((360.0 / (65536.0 * 65536.0)) / Math.Pow(2, 32))) & ~0xFFFF);
                        bw.Write((Int64)(doubleData[2] * Math.Pow(2, 32)) & ~0xFFFF);
                    }
                    break;

                case Type.PBH:
                    {
                        var floatData = this.Float;
                        bw.Write((Int32)(floatData[0] / (360.0f / (65536.0f * 65536.0f))));
                        bw.Write((Int32)(floatData[1] / (360.0f / (65536.0f * 65536.0f))));
                        bw.Write((Int32)(floatData[2] / (360.0f / (65536.0f * 65536.0f))));
                        bw.Write(0); // Throw away junk
                    }
                    break;

                case Type.PropertySet:
                default:
                    throw new InvalidDataException("UNEXPECTED TYPE FOR PROPERTY");
            }
        }

        internal void ReadBinaryData(OrderedDictionary<Guid, int> idToObjectSizeTable, Format format, BinaryReader br)
        {
            // First value is an index of the localization id.  This should be 0 for everything except localized strings.
            if (format == Format.FlightBinary)
            {
                int locidIndex = br.ReadInt32();
                this.locid = idToObjectSizeTable.GetKeyByIndex(locidIndex);
            }

            // Now load the type
            switch (this.def.Type.Type)
            {
                case Type.Byte4:
                    {
                        var byteData = (byte[])this.data;
                        for (int i = 0; i < byteData.Length; ++i)
                        {
                            byteData[i] = br.ReadByte();
                        }
                    }
                    break;

                case Type.ULong:
                case Type.Enum:
                case Type.Bool:
                case Type.Long:
                case Type.Long2:
                case Type.Long3:
                case Type.Long4:
                    {
                        var intData = (int[])this.data;
                        for (int i = 0; i < intData.Length; ++i)
                        {
                            intData[i] = br.ReadInt32();
                        }
                    }
                    break;

                case Type.Filetime:
                    {
                        long b = br.ReadInt32();
                        b |= (long)br.ReadInt32() << 32;

                        this.Filetime = DateTime.FromFileTimeUtc(b);
                    }
                    break;

                case Type.Float:
                case Type.Float2:
                case Type.Float3:
                case Type.Float4:
                case Type.Matrix:
                    {
                        var floatData = (float[])this.data;
                        for (int i = 0; i < floatData.Length; ++i)
                        {
                            floatData[i] = br.ReadSingle();
                        }
                    }
                    break;

                case Type.Double:
                    {
                        var doubleData = (double[])this.data;
                        for (int i = 0; i < doubleData.Length; ++i)
                        {
                            doubleData[i] = br.ReadDouble();
                        }
                    }
                    break;

                case Type.Guid:
                    {
                        this.Guid = new Guid(br.ReadBytes(16));
                    }
                    break;

                case Type.String:
                    {
                        int byteCount = br.ReadInt32();
                        if (byteCount > 0)
                        {
                            this.data = System.Text.Encoding.UTF8.GetString(br.ReadBytes(byteCount - 1));
                            if ('\0' != br.ReadChar())
                            {
                                throw new InvalidDataException("EXPECTED STRING TERMINATOR");
                            }
                        }
                    }
                    break;

                case Type.LLA:
                    {
                        long[] l = new long[3];

                        for (int i = 0; i < 3; ++i)
                        {
                            l[i] = br.ReadInt64();
                        }
                        var doubleData = (double[])this.data;
                        doubleData[0] = (l[0] & ~0xFFFF) * (90.0 / (40007000.0 / 4.0)) / Math.Pow(2, 32);
                        doubleData[1] = (l[1] & ~0xFFFF) * (360.0 / (65536.0 * 65536.0)) / Math.Pow(2, 32);
                        doubleData[2] = (l[2] & ~0xFFFF) / Math.Pow(2, 32);
                    }
                    break;

                case Type.PBH:
                    {
                        var floatData = (float[])this.data;
                        floatData[0] = br.ReadInt32() * 360.0f / (65536.0f * 65536.0f);
                        floatData[1] = br.ReadInt32() * 360.0f / (65536.0f * 65536.0f);
                        floatData[2] = br.ReadInt32() * 360.0f / (65536.0f * 65536.0f);
                        br.ReadInt32(); // Throw away junk
                    }
                    break;

                case Type.PropertySet:
                default:
                    throw new InvalidDataException("UNEXPECTED TYPE FOR PROPERTY");
            }
        }
        #endregion

        #region Private
        private object data;
        private Guid locid;
        #endregion
    }
}
